package fanzone;

import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;
import java.io.*;
import java.math.BigInteger;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.UnknownHostException;

import bandsInTownQuery.*;

import client.Client;
import client.IClient;

import fanzone.web.*;

import security.*;
import servers.IServer;
import servers.IRegisterServer;

import lastFMQuery.Event;
import lastFMQuery.LastFMSearch;

public class FanZoneApp implements FanZoneProcessor
{
	private WPWebServer server;
	private int port;
	private String webappdir;
	@SuppressWarnings("unused")
	private String datadir;
//	private Map<String,CommunityInfo> communities;
	private List<EventInfo> localEvents;
	private String[] importEventLst = { "Input", "LastFM", "BandsInTown"};
	private String[] importResourceLst = { "File-system", "Remote URL"};

	public static final String RESOURCES_DIR = "resources";
	private static String registerServerAddress;
	private static IServer fanserver;//o rmi para chamar os metodos do nosso servidor

	protected FanZoneApp( int port, String webappdir, String datadir) {
		this.port = port;
		this.webappdir = webappdir;
		this.datadir = datadir;
//		communities = new HashMap<String,CommunityInfo>();
		localEvents = new ArrayList<EventInfo>();
	}

	/**
	 * Metodo principal
	 */
	public void doit() {
		server = new WPWebServer( port, webappdir, this);
		try {
			server.start();		// incia servidor web para fazer apresentacao da informacao

		} catch( FZException e) {
			System.err.println( e.getMessage());
			e.printStackTrace();
		}
	}
	
	private void redirect()
	{
		System.err.println("Redirecting...");
		IRegisterServer registerServer;
		String primaryServerID = null;
		String primaryServerAddress = null;
		try {
			registerServer = (IRegisterServer) Naming.lookup("//" +
					registerServerAddress + "/RegisterServer");

			System.out.print("Trying to retrieve primary server address... ");
			primaryServerID = registerServer.getPrimaryServerID();
			primaryServerAddress = registerServer.getPrimaryServerAddress();
			System.out.println("SUCCESS");
			
			System.out.print("Trying to connect to primary server " +
					primaryServerID + " @ " + primaryServerAddress + "... ");
			fanserver = (IServer) Naming.lookup("//" +
					primaryServerAddress +	"/" + primaryServerID);
			System.out.println("SUCCESS");
			server.setCustomMessage("DON'T PANIC! You have been redirected to a new server");
			
		} catch (Exception e){
			System.err.println("ERROR - Could not contact any primary server");
			server.setCustomMessage("Server appears to be down, try again later");
		}
	}
	
	/*===================================================================================
	 * Metodos da interface FanZoneProcessor
	===================================================================================*/
	public boolean login( String user, String pwd) {
		System.err.println( "login : user = " + user + "; pwd = " + pwd);
		return true;
	}

	public boolean createCommunity( String comm, String pwd) {
		boolean result = false;
		try {
			result = fanserver.addCommunity(comm, pwd);
		} catch (RemoteException e) {
			this.redirect();
		}

		return result;
	}

	public boolean joinCommunity( String comm, String pwd, String url) {
		boolean result = false;
		try {
			fanserver.joinCommunity(comm);
		} catch (RemoteException e) {
			this.redirect();
		}
		return result;
	}

	public boolean leaveCommunity( String comm) {
		boolean result = false;
		try {
			result = fanserver.unJoinCommunity(comm);
		} catch (RemoteException e) {
			this.redirect();
		}

		return result;
	}

	public List<String> listCommunities() {
		System.err.println( "listing communities :");
		List<String> communities = null;
		try {
			communities = fanserver.listCommunities();
		} catch (RemoteException e) {
			this.redirect();
		}

		Collections.sort(communities);

		return communities;
	}
	
	/**
	 * Lista alternativas de importacao de eventos
	 * NOTA: para adicionar nova opcao deve alterar resultado desta funcao
	 */
	public String[] listEventImports( ) {
		System.err.println( "list event imports");
		return importEventLst;
	}
	
	/**
	 * Lista alternativas de importacao de recursos
	 * NOTA: para adicionar nova opcao deve alterar resultado desta funcao
	 */
	public String[] listResourceImports( ) {
		System.err.println( "list resource imports");
		return importResourceLst;
	}

	/**
	 * Adiciona evento a lista de eventos
	 */
	@Override
	public void doEventImport(EventInfo event) {
		System.err.println( "do event import : event = " + event);

		localEvents.add(event); // add locally

		try {
			fanserver.addEvent(event); // add to server
		} catch (RemoteException e) {
			System.err.println("Remote exception when adding event to server");
			this.redirect();
		}
	}

	public boolean addToCommunity( String comm, String event) {
		System.err.println( "addToCommunity : comm = " + comm + "; event = " + event);
		return true;
	}

	public boolean remFromCommunity( String comm, String event) {
		System.err.println( "remFromCommunity : comm = " + comm + "; event = " + event);
		return true;
	}

	private List<EventInfo> importEventFromInput( String query) {
		List<EventInfo> events = new ArrayList<EventInfo>();
		events.add(new LocalEventInfo( "" + new Date().getTime(), query,
				"Description about event " + query, new ArrayList<ResourceInfo>()));

		return events;
	}

	/**
	 * Devolve lista de potenciais eventos a importar.
	 * @param imp Tipo de importacao
	 * @param query Query apresentada na importacao
	 */
	public List<EventInfo> searchEventImport( String imp, String query) {
		System.err.println( "searchEventImport : imp = " + imp + "; query = " + query);

		if(imp.equals("LastFM")){
			LastFMSearch lfmq = new LastFMSearch();
			Iterator<Event> it = lfmq.getQueryResults(query);
			List<EventInfo> list = new ArrayList<EventInfo>();
			while( it.hasNext()) {
				Event lfmEvent = it.next();

				// build useful info
				String artists = "";
				for(String artist : lfmEvent.getArtists().getArtist())
					artists += artist + ", ";

				String description = artists + "\n" + 
						lfmEvent.getStartDate() + ",\n" +
						lfmEvent.getVenue().getName() + ",\n" +
						lfmEvent.getVenue().getLocation().getCountry() + ",\n" +
						lfmEvent.getUrl();

				EventInfo ev = new LocalEventInfo("" + new Date().getTime(), lfmEvent.getTitle(), description, new ArrayList<ResourceInfo>());
				try {
					URL image;
					if(lfmEvent.getImage().size() > 2) // try to get medium img
						image = new URL(lfmEvent.getImage().get(2).getValue());
					else
						image = new URL(lfmEvent.getImage().get(0).getValue());
					ev.getResourceInfo().add(new RemoteResourceInfo(image));
				} catch (MalformedURLException e) {
					e.printStackTrace();
				}
				list.add(ev);
			}
			return list;
		}
		
		if(imp.equals("BandsInTown")){
			BandsInTownSearch bits = new BandsInTownSearch();
			Iterator<bandsInTownQuery.Event> it = bits.getQueryResults(query);
			List<EventInfo> list = new ArrayList<EventInfo>();
			while( it.hasNext()) {
				bandsInTownQuery.Event bitEvent = it.next();

				// build useful info
				String artists = "";
				for(Artist artist : bitEvent.getArtists().getArtist())
					artists += artist.getName() + ", ";
				
				String description =  artists + "\n" + 
						bitEvent.getDatetime() + ",\n" +
						bitEvent.getVenue().getName() + ",\n" +
						bitEvent.getVenue().getCity() + ",\n" +
						bitEvent.getVenue().getCountry() + ",\n" +
						bitEvent.getUrl();

				EventInfo ev = new LocalEventInfo("" + new Date().getTime(), bitEvent.getVenue().getName(), description, new ArrayList<ResourceInfo>());
				list.add(ev);
			}
			return list;
		}

		if( imp.equalsIgnoreCase( importEventLst[0]))		// INPUT
			return importEventFromInput( query);

		return new ArrayList<EventInfo>();
	}

	private List<ResourceInfo> importResourceFromFileSystem( String query) {
		if( query.equals(""))
			query = ".";
		List<ResourceInfo> res = new ArrayList<ResourceInfo>();
		File f = new File( query);
		File []arr = f.listFiles();
		if( arr != null)
			for( int i = 0; i < arr.length; i++) {
				if( arr[i].getName().startsWith("."))
					continue;
				if( arr[i].isDirectory())
					continue;
				res.add( new LocalResourceInfo( arr[i], true));
			}
		return res;
	}

	// lists the resource found for the given query
	// so the user can decide whether to import or not
	private List<ResourceInfo> importRemoteResource (String query)
	{
		List<ResourceInfo> resources = new ArrayList<ResourceInfo>();

		try {
			URL url = new URL(query);
			resources.add(new RemoteResourceInfo(url));
		} catch (MalformedURLException e) {
			System.err.println("Remote Resource Import Malformed Exception");
			e.printStackTrace();
		}
		return resources; // must return a list
	}

	private void makeCopy(InputStream stream, String fileName)
	{
		new File(RESOURCES_DIR).mkdir(); // create directory if doesn't exist
		
		try {
			OutputStream os = new FileOutputStream(new File(RESOURCES_DIR +
					"/" + fileName));

			int read = 0;
			byte[] bytes = new byte[1024];
			
			while((read = stream.read(bytes)) != -1)
				os.write(bytes, 0, read);
			
			stream.close();
			os.flush();
			os.close();
		} catch (IOException e) {
			server.setCustomMessage("Could not copy the file: I/O error");
		}
	}
	
	private void makeCopy(URL url) // for remotely imported resources
	{
		// file name must be the same as resource ID hash
		String fileName = this.getMD5(url.toExternalForm());
		
		try {
			InputStream is = url.openStream();
			this.makeCopy(is, fileName);
		} catch (IOException e) {
			server.setCustomMessage("Could not copy the file: I/O error");
		} 
	}
	
	/**
	 * Devolve lista de potenciais recursos a importar.
	 * @param imp Tipo de importacao
	 * @param query Query apresentada na importacao
	 */
	public List<ResourceInfo> searchResourceImport( String imp, String query) {
		System.err.println( "searchResoruceImport : imp = " + imp + "; query = " + query);

		if( imp.equalsIgnoreCase( importResourceLst[0]))		// FILE-SYSTEM
			return importResourceFromFileSystem( query);

		if( imp.equalsIgnoreCase( importResourceLst[1]))		// REMOTE URL
			return this.importRemoteResource(query);

		return new ArrayList<ResourceInfo>();
	}

	/**
	 * Executa importacao de recurso. Se necessario copia recurso para directoria local
	 */
	public void doResourceImport( EventInfo event, ResourceInfo info, boolean makeCopy) {
		System.err.println( "do resource import : event = " + event + "; resource = " + info + "; makeCopy = " + makeCopy);

		ResourceInfo newResource = info;
		
		if(makeCopy)
		{
			// first of all copy the resource
			URL url = null;
			try {
				// must not be getHTTPURL method because it will return null
				// for local resources, raising the MalformedURL Exception
				url = new URL(newResource.getURL().get(0));
			} catch (MalformedURLException e) {
				System.err.println("Malformed exception makeCopy");
				e.printStackTrace();
			}
			
			this.makeCopy(url);
			System.err.println("Made copy of " + newResource.getURL().get(0));
			
			if(!newResource.isLocal()) // if remote, make it local
			{
				System.err.println("Turning remote resource " + newResource +
						" into local resource...");
				newResource = new LocalResourceInfo(url, true);
				System.err.println("Success: " + newResource +
						" with id " + newResource.getID());
			}

			String address = null;
			try { // add this client's address to the resource
				address = InetAddress.getLocalHost().getHostAddress();
			} catch (UnknownHostException e) {
				System.err.println("Unknown Host Exception when adding IP address");
				e.printStackTrace();
			}
			newResource.getURL().add(address); // should be added on index 1
		}
		
		// add resource to the event locally
		// will do nothing when listing remote event as it is
		// a temporary copy of the server event and not saved
		event.getResourceInfo().add(newResource);

		// _only_ add to server if resource is either remote or
		// was imported from filesystem with 'make copy' checked
		if(!newResource.isLocal() || (newResource.isLocal() && makeCopy))
		{
			try {
				String communityName = event.getCommunities().get(0);
				String eventID = event.getID();
				fanserver.addResource(communityName, eventID, newResource);
			} catch (RemoteException e1) {
				this.redirect();
			}
		}
	}

	// shows local events or remote events _making a copy_ of the latter
	public List<EventInfo> listEvents( String comm, boolean localOnly) {
		System.err.println( "listEvents : comm = " + comm + "; localOnly = " + localOnly);
		this.syncLocalEvents();
		List<EventInfo> eventList = new ArrayList<EventInfo>(); // must at least return an empty array(!)

		if(localOnly)
		{

			if(comm.equalsIgnoreCase("all"))
				eventList = this.localEvents;
			else {
				if(!this.localEvents.isEmpty())
					// get only events belonging to this community
					for(EventInfo event : this.localEvents)
						if(event.getCommunities().get(0).equals(comm))
							eventList.add(event);
			}
		}
		else {
			// list all remote events for this community
			try {
				System.err.println("»»Listing remote events««");
				eventList = fanserver.listEvents(comm);
			} catch (RemoteException e) {
				this.redirect();
			}
		}
		return eventList;
	}

	public List<EventInfo> searchEvents( String comm, boolean localOnly, String query) {
		System.err.println( "searchEvents : comm = " + comm + "; localOnly = " + localOnly + "; query = " + query);
		List<EventInfo> results = new ArrayList<EventInfo>();
		if(localOnly)
		{
			if(!this.localEvents.isEmpty()) // need to check if empty before iterate
			{
				// build list to search
				List<EventInfo> listToSearch = new ArrayList<EventInfo>();

				if(comm.equalsIgnoreCase("all"))
					listToSearch = this.localEvents;
				else {
					// get only events belonging to this community
					for(EventInfo event : this.localEvents)
						if(event.getCommunities().get(0).equals(comm))
							listToSearch.add(event);
				}

				for(EventInfo event : listToSearch) // do the search
				{
					String eventName = event.getName().toLowerCase();
					String eventDescription = event.getDescription().toLowerCase();

					if(eventName.contains(query.toLowerCase()) ||
							eventDescription.contains(query.toLowerCase()))
						results.add(event);
				}
			}
		}
		else {
			// search all remote events for this community
			try {
				System.err.println("»»Searching remote events««");
				results = fanserver.searchEvents(comm, query);
			} catch (RemoteException e) {
				this.redirect();
			}
		}

		if(results == null)
		{
			server.setCustomMessage("You do not belong in this community, therefore cannot search it");
			results = new ArrayList<EventInfo>(); // cant return null array
		}
		return results;
	}

	public InputStream getResourceStream( String id, ResourceInfo info) {
		System.err.println( "get resource stream : id = " + id + "; resource = " + info);

		InputStream is = null;
		
		try { // try fullpath on the filesystem (might be the original owner)
			
			is = new FileInputStream(id);
			
		} catch( FileNotFoundException e) {
			
			try { // try the 'resources' directory (might be one of the owners)
				
				is = new FileInputStream(RESOURCES_DIR + "/" +
						this.getMD5(info.getID()));
				
			} catch (FileNotFoundException e2) { // not on this host, go fetch
				
				String resourceIDHash = this.getMD5(info.getID());
				System.err.println(info.getURL().size() - 1 +
						" hosts with resource " +	resourceIDHash);
				for(int index = 1; index < info.getURL().size();)
				{
					String hostAddress = info.getURL().get(index);
					byte[] bytes = null;
					try {
						IClient client = (IClient) Naming.lookup("//" +
								hostAddress + "/clientServer");
						bytes = client.fetchResource(resourceIDHash);
						is = new ByteArrayInputStream(bytes);
					} catch (Exception e1) {
						is = null;
					}
					
					if(is != null)
					{
						System.err.println("Fetched resource " + resourceIDHash +
								" from host @ " + hostAddress);
						
						this.makeCopy(new ByteArrayInputStream(bytes),
								resourceIDHash);
						System.err.println("Copied fetched resource");
						
						try {
							fanserver.addResourceProvider(resourceIDHash);
						} catch (RemoteException e1) {
							System.err.println("Couldn't add this host as provider");
							this.redirect();
						}
						
						break; // success
					}
					else
						index++;
				}
			}
		}
		return is;
	}

	private void syncLocalEvents ()
	{
		List<EventInfo> serverList = null;
		System.err.println("Syncing local events with server...");
		try {
			serverList = fanserver.listEvents("All");
			Iterator<EventInfo> it = this.localEvents.iterator();
			while(it.hasNext())
			{
				EventInfo localEvent = it.next();
				boolean found = false;

				// search server events for event with same ID
				for(EventInfo serverEvent : serverList)
					if(localEvent.getID().equals(serverEvent.getID()))
						found = true;

				if(!found) // remove if not found on server (has been deleted)
				{
					System.err.println("Removed event " + localEvent.getName());
					it.remove();
				}
			}
		} catch (RemoteException e) {
			System.err.println("Remote exception when syncing local events with server");
			this.redirect();
		}
	}

	/*===================================================================================
	 * Aux methods
	===================================================================================*/
	private String getMD5(String string)
	{
		MessageDigest message = null;
		try {
			message = MessageDigest.getInstance("MD5");
		} catch (NoSuchAlgorithmException e) {
			// do nothing
		}
		message.update(string.getBytes(),0,string.length());
		
		String hash = new BigInteger(1,message.digest()).toString(16);
		
		while (hash.length() < 32)
			hash = "0" + hash;
		
		return hash;
	}
	
	/*===================================================================================
	 * Main
	===================================================================================*/
	/**
	 * @param args
	 */
	public static void main( String[] args) {
		if( args.length > 3) {
			System.err.println( "Use: java fanzone.FanZoneApp port webappdir datadir");
			return;
		}
		KeyManager.getInstance();
		KeyManager.makePublicCert("client");
		
		int port = 8080;
		String webappdir = "webapp";
		String datadir = "data";
		if( args.length > 0)
			port = Integer.parseInt( args[0]);
		if( args.length > 1)
			webappdir = args[1];
		if( args.length > 2)
			datadir = args[2];
		new File(datadir).mkdirs();

		new FanZoneApp( port, webappdir, datadir).doit();
		
		try { // initialize resource fetcher
			System.out.println("\nResource Provider Initialized");
			new Client();
		} catch (RemoteException e2) {
			e2.printStackTrace();
		}

		// ask RegisterServer for a server
		boolean served = false;
		BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
		
		while(!served)
		{
			System.out.println("Please type in the address of the register server: " +
					"(leave blank for localhost)");
			registerServerAddress = "";
			try {
				registerServerAddress = reader.readLine();
			} catch (IOException e1) {
				e1.printStackTrace();
			}
			
			if(registerServerAddress.isEmpty() ||
					registerServerAddress.matches("^(\\d{1,3}\\.){3}(\\d{1,3})$"))
			{
				if(registerServerAddress.isEmpty())
					registerServerAddress = "localhost";
				
				System.out.println("Acknowledging " + registerServerAddress + 
						" as the location of the register server");
				
				IRegisterServer registerServer;
				fanserver = null;
				String primaryServerID = null;
				String primaryServerAddress = null;
				try {
					registerServer = (IRegisterServer) Naming.lookup("//" +
							registerServerAddress + "/RegisterServer");

					System.out.print("Trying to retrieve primary server address... ");
					primaryServerID = registerServer.getPrimaryServerID();
					primaryServerAddress = registerServer.getPrimaryServerAddress();
					System.out.println("SUCCESS");
					
					System.out.print("Trying to connect to primary server " +
							primaryServerID + " @ " + primaryServerAddress + "... ");
					fanserver = (IServer) Naming.lookup("//" +
							primaryServerAddress +	"/" + primaryServerID);
					System.out.println("SUCCESS");
					served = true;
					
				} catch (MalformedURLException e) {
					System.err.println("ERROR\n");
				} catch (RemoteException e) {
					System.err.println("ERROR\n");
					e.printStackTrace();
				} catch (NotBoundException e) {
					System.err.println("ERROR\n");
				}
				
				if(fanserver == null)
				{
					System.out.println("No servers available...");
					System.out.println("------------------------------");
					break;
				}
			}
			else
			{
				System.out.println("Not a valid IP address format");
				System.out.println("------------------------------");
			}
		}
	}
}

class CommunityInfo {
	String name;
	String pwd;
	String url;
	public CommunityInfo(String name, String pwd, String url) {
		this( name, pwd);
		this.url = url;
	}
	public CommunityInfo(String name, String pwd) {
		this.name = name;
		this.pwd = pwd;
	}

}

class LocalEventInfo implements EventInfo, Serializable
{
	private static final long serialVersionUID = 1L;

	private String id;
	private String text;
	private String description;
	private List<ResourceInfo> res;
	List<String> communities;

	LocalEventInfo( String id, String text, String description, List<ResourceInfo> res) {
		this.id = id;
		this.text = text;
		this.description = description;
		this.communities = new ArrayList<String>();
		this.res = res;
	}

	@Override
	public String getID() {
		return id;
	}

	@Override
	public String getName() {
		return text;
	}

	@Override
	public String getDescription() {
		return description;
	}

	@Override
	public List<ResourceInfo> getResourceInfo() {
		return res;
	}

	public List<String> getCommunities() {
		return communities;
	}

	@Override
	public void setName(String str) {
		text = str;

	}

	@Override
	public void setDescription(String str) {
		description = str;		
	}
}
